package com.hmdp.utils;

import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import com.hmdp.dto.Result;
import com.hmdp.entity.ObjectExpire;
import com.hmdp.entity.Shop;
import com.hmdp.entity.ShopExpire;
import io.netty.util.internal.StringUtil;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;

import java.time.LocalDateTime;
import java.util.Objects;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.function.Function;


/**
 * 缓存管理：处理缓存击穿与缓存穿透
 * 1.缓存击穿：设置 逻辑过期时间
 * 2.缓存穿透：设置 TTL销毁时间
 */
@Component
public class CacheManager {
    //创建RedisTemplate并实现String序列化
    private StringRedisTemplate stringRedisTemplate;

    public CacheManager(StringRedisTemplate stringRedisTemplate) {
        this.stringRedisTemplate = stringRedisTemplate;
    }

    /**
     * 缓存击穿：逻辑过期时间设置,添加缓存
     *
     * @param key       缓存键
     * @param value     对应的无逻辑时间的对象实例
     * @param time     持续时间
     * @param timeUnit 时间单位
     */
    public void setLogicalTime(String key, Object value, Long time, TimeUnit timeUnit) {
        //将value转化为Bean，并构建为ObjectExpire
        ObjectExpire objectExpire = new ObjectExpire(LocalDateTime
                .now().plusSeconds(timeUnit.toSeconds(time)),
                value);
        //添加缓存
        stringRedisTemplate.opsForValue().set(
                key, JSONUtil.toJsonStr(objectExpire), time, timeUnit
        );
    }

    /**
     * 缓存穿透：设置触发缓存穿透的数据对应缓存
     *
     * @param key
     * @param value
     * @param time     持续时间
     * @param timeUnit 时间单位
     */
    public void setTTL(String key, Object value, Long time, TimeUnit timeUnit) {
        stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(value), time, timeUnit);
    }

    /**
     * 缓存击穿——使用互斥锁
     *
     * @param id
     * @param objectPrefix
     * @param lockPrefix
     * @param type
     * @param dbFallback
     * @param <R>
     * @param <ID>
     * @return
     */
    public <R, ID> R cacheBreakdownByMutex(
            ID id,
            String objectPrefix,
            String lockPrefix,
            Class<R> type,
            Function<ID, R> dbFallback) {
        //查询缓存
        String cacheStr = stringRedisTemplate
                .opsForValue().get(objectPrefix + id);
        //缓存命中
        if (!StringUtil.isNullOrEmpty(cacheStr))
            //返回
            return JSONUtil.toBean(cacheStr, type);
        try {
            //缓存不命中,访问锁
            Boolean lock = stringRedisTemplate.opsForValue()
                    .setIfAbsent(lockPrefix + id, "ok");
            //无法访问锁
            if (Boolean.FALSE.equals(lock)) {
                //阻塞等待
                Thread.sleep(50);
                return cacheBreakdownByMutex(id, objectPrefix, lockPrefix, type, dbFallback);
            }
            //能够访问锁,读取数据库并回写缓存
            try {
                Thread.sleep(10000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            //查询数据库
            R r = dbFallback.apply(id);
            //数据库无该数据，释放锁直接返回null
            if (r == null) {
                //释放锁
                stringRedisTemplate.delete(lockPrefix + id);
                return null;
            }
            //数据库有该数据，回写缓存
            stringRedisTemplate.opsForValue().set(objectPrefix + id, JSONUtil.toJsonStr(r));
            return r;
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        } finally {
            //释放锁
            stringRedisTemplate.delete(lockPrefix + id);
        }
    }


    /**
     * 缓存穿透——返回空值
     *
     * @param id
     * @param objectPrefix
     * @param type
     * @param dbFallback
     * @param <R>
     * @param <ID>
     * @return
     */
    public <R, ID> R cachePenetrationByBackNull(
            ID id,
            String objectPrefix,
            Class<R> type,
            Function<ID, R> dbFallback,
            Long time,
            TimeUnit timeUnit) {
        //读取缓存
        String cacheStr = stringRedisTemplate.opsForValue().get(objectPrefix + id);
        //缓存命中返回
        if (!StringUtil.isNullOrEmpty(cacheStr))
            return JSONUtil.toBean(cacheStr, type);
        //空值缓存，防止缓存击穿
        if (Objects.equals(cacheStr, "")) return null;
        //缓存未命中，查询数据库
        R r = dbFallback.apply(id);
        //数据库未找到，报错
        if (r == null) {
            //设置TTL过期缓存
            this.setTTL(objectPrefix + id, type, time, timeUnit);
            return null;
        }
        //数据库找到，存入缓存
        String jsonShop = JSONUtil.toJsonStr(r);
        //设置TTL过期缓存
        this.setTTL(objectPrefix + id, type, time, timeUnit);
        //返回
        return r;
    }

    /**
     * 逻辑过期时间 解决 缓存击穿 问题
     * @param id
     * @param objectPrefix
     * @param lockPrefix
     * @param type
     * @param dbFallback
     * @param time
     * @param timeUnit
     * @return
     * @param <R>
     * @param <ID>
     */
    public <R, ID> R cacheBreakdownByLogicalExpire(
            ID id,
            String objectPrefix,
            String lockPrefix,
            Class<R> type,
            Function<ID, R> dbFallback,
            Long time,
            TimeUnit timeUnit
    ) {
        //访问锁
        Boolean lock = stringRedisTemplate.opsForValue().setIfAbsent(lockPrefix + id, "ok");
        //查询缓存
        String cacheStr = stringRedisTemplate.opsForValue().get(objectPrefix + id);
        if (Boolean.FALSE.equals(lock)) {
            //无法访问锁，查询缓存并返回
            //缓存为空，直接返回报错
            if (cacheStr == null) return null;
            //不为空，返回缓存对应shop
            ObjectExpire objectExpire = JSONUtil.toBean(cacheStr, ObjectExpire.class);
            JSONObject data = (JSONObject) objectExpire.getData();
            R r = JSONUtil.toBean(data, type);
            return r;
        }
        //可以访问锁
        //判断缓存是否为空，为空返回null并创建线程重写缓存
        if (cacheStr == null) {
            ExecutorService threadPool = Executors.newCachedThreadPool();
            threadPool.execute(() -> {
                saveShopExpire2Redis(objectPrefix,type,id, dbFallback, time,timeUnit);
                //释放锁
                stringRedisTemplate.delete(lockPrefix + id);
            });
            return null;
        }
        //缓存不为空，判断是否过期
        ObjectExpire objectExpire = JSONUtil.toBean(cacheStr, ObjectExpire.class);
        Object data1 = objectExpire.getData();
        JSONObject data = (JSONObject) objectExpire.getData();
        //当前缓存数据
        R r = JSONUtil.toBean(data, type);
        if (objectExpire.getExpireTime().isAfter(LocalDateTime.now())) {
            //没有过期，释放锁并返回缓存内容
            stringRedisTemplate.delete(lockPrefix + id);
            return r;
        }
        //过期，创建新线程，让新线程完成创建，返回旧的缓存内容
        ExecutorService threadPool = Executors.newCachedThreadPool();
        threadPool.execute(() -> {
            //回写数据，完成后释放锁
            saveShopExpire2Redis(objectPrefix,type,id, dbFallback, time,timeUnit);
            stringRedisTemplate.delete(lockPrefix + id);
        });
        //返回旧的缓存内容
        return r;
    }

    /**
     * 重建缓存_ 逻辑过期时间 解决 缓存击穿 问题
     *
     * @param id
     */
    public <R, ID> R saveShopExpire2Redis
    (String objectPrefix,
     Class<R> type,
     ID id,
     Function dbFallback,
     Long time,
     TimeUnit timeUnit
    ) {
        //数据库查询店铺信息
        R r = (R) dbFallback.apply(id);
        //添加逻辑时间缓存
        setLogicalTime(objectPrefix + id, r, time, timeUnit);
        return r;
    }

}

